performance.now() が monotonic (単調増加) なことを利用すると、システム時計の変化を比較的高精度に得られるなと思ったので、以下のようなClockMonitorクラスを作ってみた。
// ClockMonitor: システム時計の大幅な変更(NTP補正・手動変更等)を検知し、イベントを発行するクラス
// WebAudioやperformance.now()はmonotonicな経過時間だが、絶対時刻(Date.now())はシステム時計依存でジャンプすることがある
// そのため、performance.timeOrigin+performance.now()で絶対時刻を計算している場合、
// システム時計が変化しても自動で補正されない(ズレたままになる)
// このクラスは、定期的にDate.now()とperformance.timeOrigin+performance.now()の差分を監視し、
// 一定以上の差分が発生した場合に"clockchange"イベントを発行することで、
// 利用側がoffset等を補正できるようにする
class ClockMonitor extends EventTarget {
constructor({ threshold = 2000, interval = 1000 } = {}) {
super();
this.threshold = threshold; // 何ms以上の差分で検知するか
this.interval = interval; // 監視間隔(ms)
this.offset = performance.timeOrigin || 0; // performance.now()の起点(初期化時の絶対時刻)
this._timer = null;
}
start() {
if (this._timer) return;
this._timer = setInterval(() => {
const perfNow = performance.now();
const now = Date.now();
// 現在の絶対時刻の期待値(初期offset+経過時間)
const expected = this.offset + perfNow;
const diff = now - expected;
// threshold以上の差分が出たらシステム時計変更とみなす
if (Math.abs(diff) > this.threshold) {
// offsetを補正し、イベント発行
this.offset += diff;
this.dispatchEvent(new CustomEvent("clockchange", {
detail: { offset: this.offset, diff }
}));
}
}, this.interval);
}
stop() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
}
}
他の方法
Date.now() を単に保持しておいて比較することでも、過去への遡りは検出できる。が、未来へ進むのは検出できない (ただのタイマーの遅れと区別できない)
問題点
問題点: performance.now() は monotonic ではあるがスリープで時間の連続性が失われることがある。
仕様上は連続することになっているが、一部の環境のブラウザだけ。